/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.beanSpec;

import java.beans.*;
import java.util.*;
import org.jdesktop.observablecollections.*;
import ch.trackedbean.loaders.lazy.*;
import ch.trackedbean.util.*;

/**
 * Class holding the status of a bean and it's properties.
 * 
 * @param <T> The owner type
 * @author M. Hautle
 */
public class BeanStatusImpl<T> implements ChildStatus, IBeanStatus<T> {
	/** Object representing null */
	private static final Object NULL = new Object();

	/** Default status (use if no status is set) */
	private static final Status DEFAULT_STATUS = new Status(BeanStatusFlag.UNCHANGED, null);

	/** The owner object of this Status object */
	private final T owner;

	/** A map holding the status of all properties */
	private final ObservableMap<String, Status> status = ObservableCollections.observableMap(new HashMap<String, Status>());

	/**
	 * A map holding the initial values of all properties.<br>
	 * The initial value of a property is set when the first time {@link #propertyChanged(String, Object, Object)} is called. {@link #NULL} replaces null so
	 * that there is a difference between null and not yet set.
	 */
	private final HashMap<String, Object> initialValues = new HashMap<String, Object>();

	/** A list with structure parents or null */
	private ArrayList<ChildStatus> structureParents = null;

	/** The number of properties with the {@link BeanStatusFlag#ERROR} flag */
	private int errorCount = 0;

	/** The number of properties with the {@link BeanStatusFlag#CHANGED} flag */
	private int changedCount = 0;

	/**
	 * The number of childs containing errors in their structure ({@link BeanStatusFlag#ERROR} flag)<br>
	 * The status property of this bean is handled like the status of a child.
	 */
	private int childErrorCount = 0;

	/**
	 * The number of childs containing changes in their structure ({@link BeanStatusFlag#CHANGED} flag)<br>
	 * The status property of this bean is handled like the status of a child.
	 */
	private int childChangedCount = 0;

	/**
	 * Default constructor
	 * 
	 * @param owner The owner Bean of this Status object
	 */
	public BeanStatusImpl(final T owner) {
		this.owner = owner;
		status.put(BEAN_STATUS, new Status(BeanStatusFlag.UNCHANGED));
		status.put(BEAN_STRUCTURE_STATUS, new Status(BeanStatusFlag.UNCHANGED));
	}

	/**
	 * Returns the {@link BeanStatusImpl} of a bean
	 * 
	 * @param <T> The bean type
	 * @param bean The bean
	 * @return The beanstatus or null if there is none
	 */
	@SuppressWarnings("unchecked")
	public static <T> IBeanStatus<T> getBeanStatus(final T bean) {
		if (bean instanceof StatusInformationInternal)
			return ((StatusInformationInternal) bean)._getBeanStatus();
		return null;
	}

	/**
	 * Resets the status information of the given bean by calling {@link #clearInitialValues(boolean)}.<br>
	 * Use this methode after initializing a Bean.
	 * 
	 * @param bean The bean
	 */
	public static void resetBeanStatus(final Object bean) {
		if (bean instanceof StatusInformationInternal)
			getBeanStatus(bean).clearInitialValues(true);
	}

	/**
	 * Resets the properties of the given bean to their initial value.
	 * 
	 * @param bean The bean
	 * @throws ResetException If something went wrong
	 */
	public static void resetToInitialValues(final Object bean) throws ResetException {
		if (bean instanceof StatusInformationInternal)
			getBeanStatus(bean).resetToInitialValues();
	}

	/**
	 * Returns the {@link BeanStatusImpl} of a nested Bean
	 * 
	 * @param bean The base bean
	 * @param path The path to a Bean relative to the basebean
	 * @return The {@link BeanStatusImpl} of the bean at the end of the given path or null if there is none
	 */
	public static IBeanStatus getBeanStatus(Object bean, final String path) {
		if (path.length() > 0)
			bean = ELHelper.getValueOf(path, bean);
		return getBeanStatus(bean);
	}

	/**
	 * Returns the status of the property at the end of the given path
	 * 
	 * @param bean The base bean
	 * @param path The path to the property in which status youre interested in
	 * @return The status of the property or null
	 */
	public static Status getPropertyStatus(final Object bean, final String path) {
		try {
			final int lastIndex = path.lastIndexOf('.');
			return getBeanStatus(bean, lastIndex > 0 ? path.substring(0, lastIndex) : "").getStatus(path.substring(lastIndex + 1));
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Status getStatus(final String property) {
		final Status s = status.get(property);
		return s == null ? DEFAULT_STATUS : s;
	}

	/**
	 * Checks if the given field is an uninitialized (lazy) field.<br>
	 * <b>The lazy status will be resetted by this call!</b><br>
	 * <br>
	 * <b>This method is only for internal use!</b>
	 * 
	 * @param property The name of the property
	 * @return True if the property has to be loaded
	 */
	public boolean loadField(final String property) {
		final Status s = status.get(property);
		if (s == null || s.flag != BeanStatusFlag.UNDEFINED)
			return false;
		status.remove(property);
		return true;
	}

	/**
	 * Sets the field state of the given property to {@link BeanStatusFlag#UNDEFINED} so that lazy loading will be acitvated on this field (if the field was
	 * annotated with {@link LoadLazy}).<br>
	 * 
	 * @param property The name of the property
	 */
	public void setLazy(final String property) {
		setStatus(property, BeanStatusFlag.UNDEFINED);
	}

	/**
	 * Registers the given child as structure child and unregisters the old one.
	 * 
	 * @param old The previously registred child or null
	 * @param child The new child to register or null
	 */
	public void registerChild(final Object old, final Object child) {
		if (old instanceof StatusInformationInternal)
			((StatusInformationInternal) old)._getBeanStatus().unregisterOwner(this);
		if (child instanceof StatusInformationInternal)
			((StatusInformationInternal) child)._getBeanStatus().registerOwner(this);
	}

	/**
	 * Creates a {@link StatusList} for the new list and register it as structure child. The old list will be unregistred.
	 * 
	 * @param old The previously registred list or null
	 * @param child The new list to register or null
	 * @return The new list as {@link StatusList} or null if null was passed
	 */
	@SuppressWarnings("unchecked")
	public List registerChildList(final List old, final List child) {
		if (old != null)
			StatusList.removed(old, this);
		return child == null ? null : StatusList.create(child, this);
	}

	/**
	 * Adds the given owner to the {@link #structureParents} list and calls {@link #childStatusChanged(BeanStatusFlag, BeanStatusFlag)} on the added owner with
	 * the current state and {@link BeanStatusFlag#UNCHANGED} as old state
	 * 
	 * @param owner The owner to add
	 */
	synchronized void registerOwner(final ChildStatus owner) {
		if (structureParents == null)
			structureParents = new ArrayList<ChildStatus>(1);
		structureParents.add(owner);
		owner.childStatusChanged(BeanStatusFlag.UNCHANGED, status.get(BEAN_STRUCTURE_STATUS).flag);
	}

	/**
	 * Removes the given owner form the {@link #structureParents} list and calls {@link #childStatusChanged(BeanStatusFlag, BeanStatusFlag)} on the remove owner
	 * setting the new state to {@link BeanStatusFlag#DELETED}
	 * 
	 * @param owner The owner to remove
	 */
	synchronized void unregisterOwner(final ChildStatus owner) {
		if (!structureParents.remove(owner))
			throw new IllegalStateException("Tryed to remove " + owner + " from the structure parents list, but it was alredy removed!");
		owner.childStatusChanged(status.get(BEAN_STRUCTURE_STATUS).flag, BeanStatusFlag.DELETED);
		if (structureParents.size() == 0)
			structureParents = null;
	}

	/**
	 * Called by the owning bean if a property has changed
	 * 
	 * @param property The property name
	 * @param oldValue The old value
	 * @param newValue The new value
	 */
	public void propertyChanged(final String property, final Object oldValue, final Object newValue) {
		// ignore same values
		if (oldValue == newValue || oldValue != null && oldValue.equals(newValue)) {
			resetTechnicalError(property, newValue);
			return;
		}
		Object init = initialValues.get(property);
		// first property change -> set initial property
		if (init == null) {
			initialValues.put(property, oldValue == null ? NULL : oldValue);
			setStatus(property, BeanStatusFlag.CHANGED);
			return;
		}
		// replace null placeholder
		if (init == NULL)
			init = null;
		setStatus(property, init == newValue || init != null && init.equals(newValue) ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED);
	}

	/**
	 * Resets an {@link BeanStatusFlag#TECHNICAL_ERROR} on the given property and sets it on {@link BeanStatusFlag#CHANGED} or {@link BeanStatusFlag#UNCHANGED}
	 * depending on the property value.
	 * 
	 * @param property The property
	 * @param newValue The new value
	 */
	private void resetTechnicalError(final String property, final Object newValue) {
		final Status s = status.get(property);
		if (s == null || s.getFlag() != BeanStatusFlag.TECHNICAL_ERROR)
			return;
		Object init = initialValues.get(property);
		// if init wasn't set then the property wasn't yet touched (before the error), so now it's unchanged again
		if (init == null) {
			setStatus(property, BeanStatusFlag.UNCHANGED);
			return;
		}
		// replace null placeholder
		if (init == NULL)
			init = null;
		setStatus(property, init == newValue || init != null && init.equals(newValue) ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED);
	}

	/**
	 * {@inheritDoc}
	 */
	public void setStatus(final String property, final BeanStatusFlag flag) {
		setStatus(property, flag, null);
	}

	/**
	 * {@inheritDoc}
	 */
	public void setStatus(final String property, final BeanStatusFlag flag, final String message) {
		// don't touch the bean status
		if (property == BEAN_STATUS)
			return;
		Status old = status.get(property);
		// if no value was set then use the default value as old value
		if (old == null)
			old = DEFAULT_STATUS;
		// if the old status corresponds to the new one, then do nothing
		if (old.equals(flag, message))
			return;
		status.put(property, new Status(flag, message));
		// compute the new error and changed counts
		errorCount += flag.errorValue - old.flag.errorValue;
		changedCount += flag.changedValue - old.flag.changedValue;
		// update the beans status
		final BeanStatusFlag oldStatus = status.get(BEAN_STATUS).getFlag();
		// ignore changes on deleted beans...
		if (oldStatus != BeanStatusFlag.DELETED) {
			if (errorCount == 0) {
				final BeanStatusFlag newFlag = changedCount == 0 ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED;
				if (newFlag != oldStatus)
					setBeanStatus(new Status(newFlag));
			} else if (BeanStatusFlag.ERROR != oldStatus) {
				setBeanStatus(new Status(BeanStatusFlag.ERROR));
			}
		}
	}

	/**
	 * Sets the bean status and calls {@link #childStatusChanged(BeanStatusFlag, BeanStatusFlag)} with the old and the new bean status (the bean status is
	 * handled like a child of the bean structure)
	 * 
	 * @param newStatus The new bean status
	 */
	private void setBeanStatus(final Status newStatus) {
		final Status old = status.put(BEAN_STATUS, newStatus);
		// inform the sturcute status about the state change
		childStatusChanged(old.flag, newStatus.flag);
	}

	/**
	 * <b>Only for internal use!</b><br>
	 * This method is public as implementation side effect, so please don't call it!
	 * 
	 * @param oldStatus The old status of the child
	 * @param newStatus The new status of the child
	 */
	public void childStatusChanged(final BeanStatusFlag oldStatus, final BeanStatusFlag newStatus) {
		if (oldStatus == newStatus)
			return;
		childErrorCount += newStatus.errorValue - oldStatus.errorValue;
		childChangedCount += newStatus.changedValue - oldStatus.changedValue;
		// ignore changes on deleted beans...
		if (status.get(BEAN_STATUS).getFlag() == BeanStatusFlag.DELETED)
			return;
		// update the beansStructure status
		final BeanStatusFlag oldBeanStatus = status.get(BEAN_STRUCTURE_STATUS).getFlag();
		if (childErrorCount == 0) {
			final BeanStatusFlag newFlag = childChangedCount == 0 ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED;
			if (newFlag != oldBeanStatus)
				setBeanStructureStatus(new Status(newFlag));
		} else if (BeanStatusFlag.ERROR != oldBeanStatus) {
			setBeanStructureStatus(new Status(BeanStatusFlag.ERROR));
		}
	}

	/**
	 * Sets the bean structure status and informs the owners about the change
	 * 
	 * @param newStatus The new bean structure status
	 */
	private void setBeanStructureStatus(final Status newStatus) {
		final Status old = status.put(BEAN_STRUCTURE_STATUS, newStatus);
		// inform the owners status about the state change
		if (structureParents != null)
			for (final ChildStatus o : structureParents)
				o.childStatusChanged(old.flag, newStatus.flag);
	}

	/**
	 * {@inheritDoc}
	 */
	public void forceBeanStatus(final BeanStatusFlag flag, final String message) {
		// workaround cause clear throws a java.util.ConcurrentModificationException :(
		final Set<String> ks = status.keySet();
		final String[] keys = ks.toArray(new String[ks.size()]);
		for (final String s : keys)
			if (!(BEAN_STATUS.equals(s) || BEAN_STRUCTURE_STATUS.equals(s)))
				status.remove(s);
		errorCount = 0;
		changedCount = 0;
		setBeanStatus(new Status(flag, message));
	}

	/**
	 * {@inheritDoc}
	 */
	public void resetToInitialValues() throws ResetException {
		try {
			for (PropertyDescriptor p : Introspector.getBeanInfo(owner.getClass()).getPropertyDescriptors()) {
				Object v = initialValues.get(p.getName());
				if (v == null)
					continue;
				// replace null placeholder
				if (v == NULL)
					v = null;
				p.getWriteMethod().invoke(owner, v);
			}
		} catch (Exception e) {
			throw new ResetException("Error while resetting the values", e);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void clearInitialValues() {
		clearInitialValues(false);
	}

	/**
	 * {@inheritDoc}
	 */
	public void clearInitialValues(boolean resetState) {
		initialValues.clear();
		if (resetState)
			forceBeanStatus(BeanStatusFlag.UNCHANGED, null);
	}

	/**
	 * {@inheritDoc}
	 */
	public Status getStatus() {
		return status.get(BEAN_STATUS);
	}

	/**
	 * {@inheritDoc}
	 */
	public Status getStructureStatus() {
		return status.get(BEAN_STRUCTURE_STATUS);
	}

	/**
	 * Returns the status map.<br>
	 * For performance reasons the original map is returned, so please don't modify it.
	 * 
	 * @return The status map
	 */
	public ObservableMap<String, Status> getStatusMap() {
		return status;
	}

	/**
	 * {@inheritDoc}
	 */
	public T getOwner() {
		return owner;
	}
}
